﻿/*
 * Catalyst - http://catalystobjects.com
 * 
 * New BSD License (BSD)
 * 
 * Copyright (c) 2009, Ashley Brener
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
 * 
 * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
 * 
 * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
 * 
 * Neither the name of Midlet Earth nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Web.Caching;
using Catalyst.Caching;
using SubSonic.DataProviders;
using SubSonic.Linq.Structure;
using SubSonic.Schema;
using System.Data.SqlClient;
using System.Text;
using System.Security.Permissions;
using System.Runtime.Caching;
using System.Web;
using System.Configuration;

namespace Catalyst.Linq
{
	public class CacheQueryProvider : DbQueryProvider, IQueryProvider
	{
		private readonly IDatabase _db;
		private ITable table;
		private string tableName;
		private Expression _expression;
		private bool isCachedSqlTable;

		private static Dictionary<string, object> nonWebCachedTables = new Dictionary<string, object>();

		public CacheQueryProvider(IDatabase db)
			: base(db.Provider)
		{
			_db = db;
		}

		public static void RemoveCachedTable(string dbName, string tableName)
		{
			MemoryCache.Default.Remove(String.Format("{0}_{1}", dbName, tableName));
		}

		#region IQueryProvider Members

		IQueryable<S> IQueryProvider.CreateQuery<S>(Expression expression)
		{
			return new Query<S>(this, expression);
		}

		IQueryable IQueryProvider.CreateQuery(Expression expression)
		{
			Type elementType = TypeHelper.GetElementType(expression.Type);
			try
			{
				return (IQueryable)Activator.CreateInstance(typeof(Query<>).MakeGenericType(elementType), new object[] { this, expression });
			}
			catch (TargetInvocationException tie)
			{
				throw tie.InnerException;
			}
		}

		S IQueryProvider.Execute<S>(Expression expression)
		{
			bool isEnumerable = (typeof(S).Name == "IEnumerable`1");

			Type elementType = TypeHelper.GetElementType(expression.Type);
			table = Mapping.Language.DataProvider.FindTable(elementType.Name);

			if (table == null)
			{
				// type S is not comprised of IBusinessObjects
				// Must be returned from DB
				return (S)base.Execute(expression);
			}
			else
			{
				tableName = table.Name;

				Expression queryExpression = expression;

				AspNetSqlCacheEnforcer enforcer = new AspNetSqlCacheEnforcer(_db);

				if (ConfigurationManager.AppSettings["CatalystCacheDB"] == null)
				{
					throw new Exception("\"CatalystCacheDB\" AppSetting not specified in application configuration.");
				}

				isCachedSqlTable = Boolean.Parse(ConfigurationManager.AppSettings["CatalystCacheDB"]) && enforcer.CheckIfCachedTable(tableName);

				if (isCachedSqlTable)
				{
					MethodCallExpression whereExpression = null;
					//MethodCallExpression selectExpression = null;

					// The expression must represent a query over the data source.
					if (isQueryOverDataSource(expression))
					{
						// Find the call to Where() and get the lambda expression predicate.
						InnermostWhereFinder whereFinder = new InnermostWhereFinder();
						whereExpression = whereFinder.GetInnermostWhere(expression);

						if (whereExpression != null)
						{
							Type t = whereExpression.Arguments[0].Type.GetGenericArguments()[0];

							if (whereExpression.Arguments[0].NodeType == ExpressionType.Constant && t != elementType)
							{
								// If the expression method is a Select and the return type does not match the
								// queryable type, return from database
								return (S)base.Execute(expression);
							}

							MethodCallExpression mce = expression as MethodCallExpression;

							_expression = getPrimaryTableExpression(mce);

							if (mce.Arguments.Count > 1)
							{
								queryExpression = mce.Arguments[1];
							}
						}
						else
						{
							// If the expression is not a Where expression, return from database
							return (S)base.Execute(expression);
						}
					}
					else
					{
						_expression = expression;
					}

					// Call CacheManager and get the results.
					object obj = CacheManager.Get(getCacheKey(), getAllCachedData, getCacheItemPolicy);
					
					IList items = null;

					if (obj is IEnumerable)
					{
						items = (IList)obj;
					}
					else
					{
						items = new List<S>();
						items.Add(obj);
					}

					IQueryable queryableItems = null;

					if (items.Count > 0)
					{
						IList clonedItems = ((IBusinessObject)items[0]).GetEmptyCollection();

						foreach (var item in items)
						{
							clonedItems.Add(((ICloneable)item).Clone());
						}

						// Copy the IEnumerable places to an IQueryable.
						queryableItems = clonedItems.AsQueryable();
					}
					else
					{
						queryableItems = items.AsQueryable();
					}

					if (isQueryOverDataSource(expression))
					{
						LambdaExpression lambdaExpression = (LambdaExpression)((UnaryExpression)(whereExpression.Arguments[1])).Operand;

						// Send the lambda expression through the partial evaluator.
						lambdaExpression = (LambdaExpression)Evaluator.PartialEval(lambdaExpression);

						// Copy the expression tree that was passed in, changing only the first
						// argument of the innermost MethodCallExpression.
						ExpressionTreeModifier treeCopier = new ExpressionTreeModifier(queryableItems, elementType);
						Expression newExpressionTree = treeCopier.CopyAndModify(expression);

						// This step creates an IQueryable that executes by replacing Queryable methods with Enumerable methods.
						if (isEnumerable)
						{
							return (S)queryableItems.Provider.CreateQuery(newExpressionTree);
						}
						else
						{
							return (S)queryableItems.Provider.Execute(newExpressionTree);
						}
					}

					return (S)queryableItems;
				}

				// Must be returned from DB
				return (S)base.Execute(expression);
			}
		}

		private object getFreshData()
		{
			object obj = base.Execute(_expression);

			if (!nonWebCachedTables.ContainsKey(tableName))
			{
				nonWebCachedTables.Add(tableName, obj);
			}
			else
			{
				nonWebCachedTables[tableName] = obj;
			}

			return obj;
		}

		void dependency_OnChange(object sender, SqlNotificationEventArgs e)
		{
			if (e.Type == SqlNotificationType.Change)
			{
				getFreshData();

				_db.NotifyTableChanged(tableName);
			}
		}

		object IQueryProvider.Execute(Expression expression)
		{
			return base.Execute(expression);
		}

		#endregion

		private bool isQueryOverDataSource(Expression expression)
		{
			// If expression represents an unqueried IQueryable data source instance,
			// expression is of type ConstantExpression, not MethodCallExpression.
			return (expression is MethodCallExpression);
		}

		private Expression getPrimaryTableExpression(MethodCallExpression mce)
		{			
			foreach (Expression e in mce.Arguments)
			{
				MethodCallExpression m = e as MethodCallExpression;

				if (m != null)
				{
					return getPrimaryTableExpression(m);
				}

				return e;
			}

			return mce.Arguments[0];
		}

		private string getCacheKey()
		{
			return String.Format("{0}_{1}", _db.Name, tableName);
		}

		private object getAllCachedData()
		{
			return base.Execute(_expression);
		}

		private CacheItemPolicy getCacheItemPolicy()
		{
			CacheItemPolicy policy = new CacheItemPolicy
			{
				RemovedCallback = (args) =>
				{
					args.Source.Add(getCacheKey(), getAllCachedData(), getCacheItemPolicy());
				}
			};

			if (HttpContext.Current != null)
			{
				policy.ChangeMonitors.Add(new AspNetSqlChangeMonitor(_db.Name, tableName));
			}
			else
			{
				policy.SlidingExpiration = TimeSpan.FromMinutes(30);
			}

			return policy;
		}

		//internal class LocationFinder : ExpressionVisitor
		//{
		//    private Expression expression;
		//    private List<string> locations;

		//    public LocationFinder(Expression exp)
		//    {
		//        this.expression = exp;
		//    }

		//    public List<string> Locations
		//    {
		//        get
		//        {
		//            if (locations == null)
		//            {
		//                locations = new List<string>();
		//                this.Visit(this.expression);
		//            }
		//            return this.locations;
		//        }
		//    }

		//    protected override Expression VisitBinary(BinaryExpression be)
		//    {
		//        if (be.NodeType == ExpressionType.Equal)
		//        {
		//            if (ExpressionTreeHelpers.IsMemberEqualsValueExpression(be, typeof(Place), "Name"))
		//            {
		//                locations.Add(ExpressionTreeHelpers.GetValueFromEqualsExpression(be, typeof(Place), "Name"));
		//                return be;
		//            }
		//            else if (ExpressionTreeHelpers.IsMemberEqualsValueExpression(be, typeof(Place), "State"))
		//            {
		//                locations.Add(ExpressionTreeHelpers.GetValueFromEqualsExpression(be, typeof(Place), "State"));
		//                return be;
		//            }
		//            else
		//                return base.VisitBinary(be);
		//        }
		//        else
		//            return base.VisitBinary(be);
		//    }
		//}


	}



	



	
}